import {HtmlNode} from "./HtmlNode"
import {Bounds, IContext, ICreateNodeData, INodeData, INodeImpl, ThemeName} from "./interfaces.ts"
import {collectJsxEvents, disposeJsxEvents} from "@/utils/jsx-runtime.ts"
import {eachNode, makeUUID} from "./utils.ts";
import {Stage} from "./Stage.ts";

let INST_ID_INC = 0

export class NodeWrapper extends HtmlNode {
  protected _instId: number = ++INST_ID_INC
  protected _nodeData: INodeData | undefined
  protected _host: HTMLElement | undefined
  protected _selected: boolean = false
  protected _overlayElEvents: any
  protected _parent?: NodeWrapper
  protected _stage?: Stage
  protected _children: NodeWrapper[] = []
  protected _globalBounds: Bounds = {x: 0, y: 0, w: 0, h: 0}
  $nodeImpl: INodeImpl | undefined
  $overlayEl: HTMLElement | undefined
  $shortcutBounds = {} as Bounds

  get instId() {
    return this._instId
  }

  get host() {
    return this._host
  }

  get selected() {
    return this._selected
  }

  get parent() {
    return this._parent
  }

  get nodeData() {
    return this._nodeData!
  }

  get id() {
    return this._nodeData!.id!
  }

  get bounds() {
    return this._nodeData!.bounds ?? {x: 0, y: 0, w: 0, h: 0}
  }

  get globalBounds() {
    return this._globalBounds
  }

  get children() {
    return this._children
  }

  $init(ctx: IContext, stage: Stage, nodeData: INodeData, impl?: INodeImpl) {
    this.$ctx = ctx
    this._stage = stage
    this._nodeData = nodeData
    this.$nodeImpl = impl

    this._createOverlay()

    this.$callSetup()

    this.$invokeCreate()
    this.$invokeUpdate()
    if (this._host) {
      this.$el.insertBefore(this._host, this.$el.lastChild)
    }
  }

  $callSetup() {
    this._host = this.$nodeImpl?.setup.apply(this, [this.$ctx!])
  }

  $invokeCreate() {
    this.$nodeImpl?.hooks?.create?.apply(this, [])
  }

  $invokeUpdate() {
    this.$nodeImpl?.hooks?.update?.apply(this, [this._nodeData?.props])
  }

  $invokeResize() {
    this.$nodeImpl?.hooks?.resize?.apply(this, [])
  }

  $invokeThemeChanged(theme: ThemeName) {
    this.$nodeImpl?.hooks?.themeChanged?.apply(this, [theme, this._nodeData?.props])
  }

  $invokeDestroy() {
    this.$nodeImpl?.hooks?.destroy?.apply(this, [])
    this.disposeEvents()
    if (this._overlayElEvents) {
      disposeJsxEvents(this._overlayElEvents)
      this._overlayElEvents = null
    }
    if (this.$overlayEl) {
      this.$el.removeChild(this.$overlayEl)
      this.$overlayEl = undefined
    }
  }

  $applyBounds() {
    const {x, y, w, h} = this._nodeData?.bounds ?? {}
    x !== undefined && (this.$el.style.left = x + 'px')
    y !== undefined && (this.$el.style.top = y + 'px')
    w !== undefined && (this.$el.style.width = w + 'px')
    h !== undefined && (this.$el.style.height = h + 'px')
    this.$invokeResize()
  }

  $updateGlobalBounds(recursion = false) {
    const parentGlobalBounds = this._parent?.globalBounds ?? {x: 0, y: 0, w: 0, h: 0}
    const {x, y, w, h} = this.bounds
    this._globalBounds.x = parentGlobalBounds.x + x
    this._globalBounds.y = parentGlobalBounds.y + y
    this._globalBounds.w = w
    this._globalBounds.h = h

    if (recursion) {
      eachNode(this, childNode => {
        childNode.$updateGlobalBounds()
      })
    }
  }

  protected _createOverlay() {
    if (this.$overlayEl) {
      return
    }
    const {htmlElement, events} = collectJsxEvents(() =>
      <div className="overlay"/>
    )
    this._overlayElEvents = events

    this.$el.appendChild(htmlElement)
  }

  protected _createEl() {
    const el = super._createEl()
    el.classList.add('node-wrapper')
    return el
  }

  addChildWithData(data: ICreateNodeData, insertIndex?: number) {
    data.pid = this.id
    const node = this._stage!.$createNode(data)
    this.addChild(node, insertIndex)
    node.$invokeResize()
    return node
  }

  addChild(childNode: NodeWrapper, insertIndex?: number) {
    if (!childNode) {
      return
    }
    childNode.$addTo(this)
    const el = this.$el
    if (insertIndex === undefined) {
      this._children.push(childNode)
      el.insertBefore(childNode.$el, el.lastChild)
    } else {
      this._children.splice(insertIndex, 0, childNode)
      if (insertIndex === 0) {
        el.prepend()
      } else {
        const previousNode = this._children[insertIndex - 1]
        el.insertBefore(childNode.$el, previousNode.$el)
      }
    }

    childNode.$applyBounds()
    childNode.$updateGlobalBounds()
    return childNode
  }

  removeChild(childNode: NodeWrapper) {
    if (!childNode) {
      return
    }
    const index = this._children.indexOf(childNode)
    if (index < 0) {
      return
    }

    childNode.$removeFromParent()
    this._children.splice(index, 1)
    const el = this.$el
    if (!el.contains(childNode.$el)) {
      return
    }
    el.removeChild(childNode.$el)
    return childNode
  }

  removeFromParent() {
    if (!this._parent) {
      return
    }
    this._parent.removeChild(this)
  }

  $addTo(parentNode: NodeWrapper) {
    this._parent = parentNode
    this._stage!.$invokeAddNode(this)
  }

  $removeFromParent() {
    this._parent = undefined
    this.$invokeDestroy()
    this._stage!.$invokeRemoveNode(this)
  }
}
